Skip to content

[lora] feat: multi lora async (full PR, see stacked diff PRs instead)#1638

Merged
yushengsu-thu merged 71 commits into
radixark:mainfrom
mathewjhan:feat/multi-lora-async
Jul 21, 2026
Merged

[lora] feat: multi lora async (full PR, see stacked diff PRs instead)#1638
yushengsu-thu merged 71 commits into
radixark:mainfrom
mathewjhan:feat/multi-lora-async

Conversation

@mathewjhan

@mathewjhan mathewjhan commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Allow disagg fully async training on multiple loras (all-linear, excluding expert) per step in Miles using megatron-bridge.

Two LoRAs trained together (DAPO + GSM8K)

image image

Feature

  • Train multiple LoRAs in a single training step
  • Use as a long running service, with in_place pause generation and LoRA upsert updates
  • Use as normal training job, stopping when no more LoRAs are left to train
  • FastAPI control plane to register and deregister adapter runs

Dependent PRs

SGLang

Unified above: sgl-project/sglang#31253 (for testing - do not need merge)

Megatron-Bridge (radixark)

Dev setup

Tested image: radixark/miles:dev-202607090055

You need to install the 3 forks:

SGLang

This fork has two small changes to SGLang (has both PRs):

  • Upsert semantics for loading LoRAs with the same lora name, to avoid having to unload; currently, unloading requires there to be zero requests for that LoRA to be loaded. When running pause generation before unload, we cannot complete more requests, when there are still running requests on unload, it is impossible to unload the adapter and miles will hang
  • Abort by rid prefix; this is useful to target specific requests under a "namespace", such as when a LoRA run has been deregistered from miles and we want to abort all of the requests for that LoRA adapter. The alternative is to track all requests for that LoRA adapter individually and send in individual abort requests, since SGLang currently doesn't support batch abort
git clone https://github.com/mathewjhan/sglang
cd sglang
git checkout sglang-miles-mathew-dev-2
uv pip install --system --break-system-packages --no-deps -e python

Megatron-Bridge

This fork adds a multi-lora support to Megatron-Bridge along with some helper methods/user api

git clone https://github.com/mathewjhan/Megatron-Bridge
cd Megatron-Bridge
git checkout radixark/bridge/multilora-2
uv pip install --system --break-system-packages --no-deps -e .

Miles

git clone https://github.com/mathewjhan/miles
cd miles
git checkout feat/multi-lora-async
uv pip install --system --break-system-packages --no-deps -e .

Running

see: examples/multi_lora

For normal training (not as a service):

  1. run examples/multi_lora/provision.sh
  2. configure W&B credentials and settings in examples/multi_lora/run_job.sh
  3. run examples/multi_lora/run_job.sh |& tee run.log

For multi-lora training as a long running service:

  1. run examples/multi_lora/provision.sh
  2. configure W&B credentials and settings in examples/multi_lora/run_service.sh
  3. run examples/multi_lora/run_service.sh |& tee run.log in one shell
  4. wait for the service to be ready ("No adapters; sleeping for 5.0s...")
  5. optionally run the smoke test in another shell
python3 examples/multi_lora/service_smoke.py \
      --api-url http://127.0.0.1:8068 \
      --data /root/gsm8k/train.parquet \
      --input-key messages \
      --label-key label \
      --rm-type math \
      --steps 5

Minor features added to existing code (backwards compatible)

  • Add AdapterRef and RewardSpec to Sample type so individual samples can access their own reward functions and adapter names during rollout

Missing features for future

  • Colocated + update_weight_from_tensor currently unsupported, only update_weight_from_distributed
  • Per adapter batch size currently unsupported, all adapters stepped at once (will be added later when decoupling the optimizer)
  • Currently doesn't checkpoint optimizer + scheduler state yet, but can be added later as future PR
  • Dataset checkpoint loading per adapter
  • MultiLoRA not applied to experts as of now due to more complex bookkeeping required (need to keep track of the [adapter index, routed experts] together for grouped gemm)
  • Doesn't support pure LoRA load from *.bin/*.safetensors yet, can be added later as a future PR, only resume from a megatron checkpoint or train from scratch
  • Support add_samples to recycle stale/aborted samples

@mathewjhan mathewjhan changed the title Feat/multi lora async feat: multi lora async Jul 12, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a fully-async multi-LoRA training framework, enabling concurrent training of multiple LoRA adapters against a shared base model. It adds a background rollout worker, an async round-robin data source, a Ray-based controller with an HTTP control-plane API, and integrates slot-keyed adapter management into the Megatron-LM and SGLang backends. The review feedback identifies a critical distributed deadlock risk in actor.py caused by non-deterministic set iteration during collective operations, which can be fixed by sorting. Additionally, the reviewer recommends replacing several assert statements with ValueError for input validation and closing the httpx.Client in the smoke test to prevent resource leaks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread miles/backends/megatron_utils/actor.py Outdated
Comment thread miles/utils/arguments.py Outdated
Comment thread examples/multi_lora/train_multi_lora_async.py Outdated
Comment thread miles/ray/multi_lora/controller.py
Comment thread examples/multi_lora/service_smoke.py
Comment thread examples/multi_lora/service_smoke.py
@yushengsu-thu yushengsu-thu self-assigned this Jul 12, 2026

@yushengsu-thu yushengsu-thu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran a deep verification pass over this branch together with its two sglang dependency PRs (sgl-project#30912/#30913); every finding below was adversarially re-verified against the code. Two issues to flag on the miles side — one is a hard crash/correctness bug in the logprob-recompute path, the other is a lifecycle race that silently corrupts rollout data across adapter slot reuse. (Side note: the deeper sglang-side findings are filed on the two dependency PRs directly.)

Comment thread miles/rollout/multi_lora/async_rollout.py
Comment thread miles/utils/multi_lora.py Outdated
_build_prefill_scoring_payload keyed LoRA solely on is_lora_enabled(args)
(always true in multi-LoRA) and sent the single-adapter name miles_lora,
which is never registered on multi-LoRA engines -> every step crashed with
'adapters are not loaded' when --recompute-logprobs-via-prefill is on; the
batched path also applied payloads[0]'s lora_path to a mixed-adapter batch.

- resolve lora_path per sample: adapter samples score under their own
  __miles_slot_{N}, single-adapter LoRA keeps miles_lora, base keeps none
- batched scoring groups by (logprob_start_len, lora_path) and rejects
  mixed-adapter batch payloads
- centralize the engine-side slot adapter name as slot_lora_name(), shared
  by rollout, prefill scoring, and the weight-push mixin

Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
The retire-time prefix abort fires exactly once (RETIRING->CLEANUP); requests
can survive it (a multi-turn group POSTs its next turn after the round, or a
request sits in the engine's tokenizer window) and, once the slot is reused
and the next adapter's weights are upserted into the same __miles_slot_{N},
keep decoding under the wrong weights with no error. Batch-time collection
filters already drop such groups from training data; these changes shrink the
window and stop wasting decode on them:

- MultiLoRABackend.free_slot: re-run the prefix abort right before the slot
  is released for reuse (second round, after a full step of settling)
- sglang_rollout.generate: refuse to POST for an adapter that is no longer
  sampleable (deregistered/cleaned up) and abort the sample locally instead
- the tokenizer-window escape itself is fixed engine-side (sgl-project#30912)

Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
@yushengsu-thu

yushengsu-thu commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

@mathewjhan examples/multi_lora/tests/test_multi_lora_async_rollout.py::test_process_group_recycles_aborted fails on this branch (pre-existing at e371b2e, unrelated to the M1/M2 fixes): the test expects aborted groups to be re-queued, but the code path documents that re-queuing is not wired up. Which direction do you want — wire up re-queuing for aborted groups, or adjust the test to pin the current drop-and-count behavior? I'm happy to pick it up once the direction is set.

Re-queuing aborted groups (add_samples) is intended but deferred to a
future PR (confirmed by mathewjhan; left unimplemented for lack of
testing time). strict=True so wiring it up forces marker removal.

Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
@yushengsu-thu

Copy link
Copy Markdown
Collaborator

Thanks for confirming — marked test_process_group_recycles_aborted as xfail(strict=True) in 50f6ff1 so the suite is green on this PR while still pinning the intended end-state behavior (wiring up re-queuing will XPASS and force the marker off). Two things worth bundling into that future PR: (1) the re-queuing itself (per-adapter source needs a write path), and (2) aborted/stale-dropped rows currently still count toward num_row, so adapters quietly train on fewer samples than configured — probably the more impactful half. Also worth skipping re-queue for RETIRING adapters (their slot is being freed; nothing can serve the regenerated group).

yushengsu-thu and others added 4 commits July 13, 2026 17:10
Each sglang tokenizer worker process holds its own LoRA registry with no
cross-worker sync (sgl-project/sglang#31084), so per-step adapter upserts
resolve on whichever worker the router picks and fail non-deterministically.
sglang rejects the upsert at runtime anyway; failing at launch avoids
burning GPU time until the first weight push.

Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
@mathewjhan
mathewjhan force-pushed the feat/multi-lora-async branch from fdefa55 to f115610 Compare July 16, 2026 06:58
…_step

The 28-line per-slot step + per-adapter scheduler + adapter_lr logging arm
moves to multi_lora_utils.step_stepped_adapter_slots; train_one_step keeps a
one-call branch. The duplicated zero-grad block (prologue + release) becomes
a local _zero_grads helper. No behavior change.
…s helpers

train_actor's mark-batch-trained block, save_model's due-adapter checkpoint
selection, and update_weights' stale-push set / version-bump bookkeeping move
to multi_lora_utils (commit_trained_batch, save_due_adapter_checkpoints,
select_adapters_to_push, commit_weight_push). Each call site keeps a short
guarded call; comments move with the bodies. No behavior change.
Comment thread examples/multi_lora/README.md
…single-LoRA transport

Restore _update_lora_weight_implementation to its main form (no new
parameters, no behavior change for single-LoRA) and give multi-LoRA its own
_update_multi_lora_weight_implementation carrying the per-slot name, the
adapter's own config, the upsert RPC, and the contiguous materialization for
sliced tensors. broadcast.py becomes a pure addition relative to main.
…dapter

The multi-LoRA push always upserts (sglang's upsert is insert-or-overwrite,
covering both first load and refresh), so the parameter threaded through
_send_one_multi_lora_adapter and the implementation was never varied.
Hardcode it at the RPC, gather the lazy imports at the top, and fold the
adapter's r/alpha into the config with a dict merge.
Comment thread miles/backends/megatron_utils/model.py Outdated
Comment thread miles/backends/megatron_utils/model.py
Comment thread miles/backends/megatron_utils/model.py
The only consumer of adapter_slots is get_batch's own per-adapter token-count
computation; the model.py callers declared it in their key lists but never
read it. Fetch it inside get_batch (None for non-multi-LoRA runs, same as the
existing dynamic_global_batch_size self-fetch) and drop it from both callers'
key lists.
# Conflicts:
#	miles/backends/megatron_utils/bridge_lora_helpers.py
Follow the run_glm5_2_744b_a40b_lora.py convention: one run_multi_lora.py
with prepare / train / full-train / serve commands over command_utils
(execute_train with train_script=train_multi_lora_async.py, model registry
qwen3-4B). provision.sh / run_job.sh / run_service.sh are gone; dataset and
model paths move to the shared /root/datasets and /root/models layout, and
the adapter yamls, smoke-client usage, and README follow.

@yushengsu-thu yushengsu-thu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summarize the comments into one or two lines. If a comment doesn't make sense, feel free to delete or remove it.

Comment thread tests/fast/backends/megatron_utils/test_multi_lora_slot_cleanup.py Outdated
Comment thread tests/fast/backends/megatron_utils/test_multi_lora_slot_cleanup.py Outdated
Comment thread tests/fast/backends/megatron_utils/test_slice_lora_to_rank.py Outdated
Comment thread tests/fast/backends/training_utils/test_get_batch_multi_lora_cp.py Outdated
Comment thread tests/fast/backends/training_utils/test_get_batch_multi_lora_cp.py Outdated
…lines

Per review: every flagged comment block across the multi-LoRA files is
reduced to at most two lines keeping only the load-bearing constraint;
redundant restatements are deleted. One error message shortened alongside.
No code change (AST-verified identical outside docstrings).
# Conflicts:
#	miles/backends/megatron_utils/arguments.py
#	miles/backends/sglang_utils/sglang_engine.py
#	miles/rollout/generate_utils/sample_utils.py
#	miles/utils/types.py
@yushengsu-thu

yushengsu-thu commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Splitting this PR into 7 stacked PRs

To reduce review load, this PR is being split into 7 module-level PRs, each an independently testable unit. This PR stays open as the integration reference (its E2E validation — 2×H200 smoke, full lifecycle — covers the whole stack). The 7 branches are pushed to mathewjhan/miles and stack on top of each other; they must be merged in order 1/7 → 7/7.

Every prefix of the stack keeps main green: the whole feature is gated behind --multi-lora-n-adapters > 0 and the driver lands last, so merging 1..k leaves everything dark until 7/7. Branch multi-lora/07-example's tree is byte-identical to this PR's head (47c3ec09) — the split is exact, no code differences.

# Branch Scope Diff CI tests carried in the PR
1/7 multi-lora/01-utils Utils foundation: types.py (AdapterRef/RewardSpec), adapter_config.py, utils/multi_lora.py (helpers + validation), CLI flags, tracking metric axes +496/−2 test_arguments.py multi-LoRA validation tests
2/7 multi-lora/02-controller Adapter controller: ray/multi_lora/ package — registry state machine, backend, control-plane HTTP API, named Ray actor +1,322 (half tests) test_controller_backend + test_controller_http (631 test lines)
3/7 multi-lora/03-trainer Trainer core: multi_lora_{optimizer,scheduler,utils}.py, model.py, bridge_lora_helpers.py, data.py/loss.py +1,255/−30 scheduler, slot_cleanup, slice_to_rank, get_batch CP=2 suites (387 test lines)
4/7 multi-lora/04-weight-sync Weight sync + actor integration: update_weight/ mixin+broadcast, sglang_engine.py upsert, actor.py reconcile/train/save/update hooks +207/−28 ⚠ no new dedicated tests — covered by main's existing weight-sync validation suite + the 7/7 E2E; the one that most needs human eyes
5/7 multi-lora/05-rollout-routing Rollout request routing: sglang_rollout.py (per-sample lora_path/rid), prefill_logprobs.py, rm_hub per-sample rewards +197/−15 test_prefill_logprobs, test_reward_config_resolution
6/7 multi-lora/06-batching Async batch collection + data conversion: rollout/multi_lora/ (async_rollout + data_source), ray/rollout/ conversions +1,267/−2 batch_collection, process_group, train_data suites (452 test lines)
7/7 multi-lora/07-example Driver + example: train_multi_lora_async.py, examples/multi_lora/ (typer launcher, adapter yamls, smoke client) +640 no fast tests; acceptance = the E2E smoke already validated on this PR

Merge order = import dependency order: everything imports 1; the trainer (3) and rollout (5, 6) talk to the controller (2); weight sync (4) uses trainer exports; the driver (7) wires it all.

Verification done on each branch: import-graph completeness per layer (no branch references a module that lands later), and each layer's fast suites pass on its own tree (layer 1: 2456 passed; layer 2: 2488; layers 3–4: 266 each incl. all existing megatron_utils tests; layer 5: 400; layer 6: full tests/fast/ray/ + rollout/).

Mechanics note: since these merge with squash, after each merge GitHub will still show the previous layers' changes in the next PR's diff until it is rebased — after merging k/7, rebase branches k+1..7 onto the new main (mechanical, no conflicts by construction).

#1742
#1743
#1744
#1745
#1746
#1747
#1748

@yushengsu-thu yushengsu-thu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All content landed via the 7 stacked PRs (#1742-#1748); this umbrella merge is a no-op content-wise and closes the loop.

@yushengsu-thu
yushengsu-thu merged commit 55861ad into radixark:main Jul 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants